package io.hellsinger.filesystem.linux.operation

import io.hellsinger.filesystem.checkScheme
import io.hellsinger.filesystem.linux.LinuxFileOperationProvider
import io.hellsinger.filesystem.linux.file.LinuxFile
import io.hellsinger.filesystem.path.Path
import io.hellsinger.filesystem.size.SizeUnit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.Closeable
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

const val DEFAULT_OPEN_MODE = 777

interface LinuxFileOpenController : Closeable {
    val position: Int
    val hasRemaining: Boolean

    val remaining: Int

    val isClosed: Boolean

    fun flip()

    fun get(): Byte

    fun get(index: Int): Byte

    suspend fun read(): Int

    suspend fun read(index: Long): Int

    suspend fun write(): Int

    fun clear()

    override fun close()
}

private class LinuxFileOpenControllerImpl(
    private val flags: Int,
    private val capacity: Int,
    private val fd: Int,
) : LinuxFileOpenController {
    override var isClosed: Boolean = false
        private set

    private val buffer = LinuxFileOperationProvider.allocate(capacity)
    private var limit = capacity
    override var position: Int = 0 // buffer position
        private set

    override val remaining: Int
        get() = limit - position

    override val hasRemaining: Boolean
        get() = position < limit

    fun checkPosition(): Boolean = position < 0 || position >= limit

    fun updateLimit(limit: Int) {
        check(limit > capacity) {
            "Limit can't be bigger than capacity (Limit: $limit; Capacity: $capacity;"
        }

        this.limit = limit
    }

    override fun flip() {
        limit = position
        position = 0
    }

    override fun clear() {
        position = 0
        limit = capacity
    }

    override fun get(): Byte {
        check(position == limit)
        return LinuxFileOperationProvider.getByte(buffer, position++)
    }

    override fun get(index: Int): Byte {
        check(index >= 0 || index >= limit)
        return LinuxFileOperationProvider.getByte(buffer, index)
    }

    override suspend fun read(): Int {
        if (!hasRemaining) return 0
        val bytes = LinuxFileOperationProvider.read(fd, position, buffer, remaining)
        position += bytes
        return bytes
    }

    override suspend fun read(index: Long): Int {
        if (!hasRemaining) return 0
        val bytes = LinuxFileOperationProvider.read(fd, position, buffer, remaining, index)
        position += bytes
        return bytes
    }

    override suspend fun write(): Int {
        if (!hasRemaining) return -1
        return LinuxFileOperationProvider.write(
            fd,
            buffer,
            remaining,
        )
    }

    override fun close() {
        LinuxFileOperationProvider.closeFileDescriptor(fd)
        LinuxFileOperationProvider.free(buffer)
        isClosed = true
    }
}

suspend fun <P : Path> LinuxFile<P>.open(
    flags: Int,
    mode: Int = DEFAULT_OPEN_MODE,
): Int =
    suspendCoroutine { continuation ->
        continuation.resume(LinuxFileOperationProvider.openFileDescriptor(path.bytes, flags, mode))
    }

suspend fun <P : Path> LinuxFile<P>.buffedOpen(
    flags: Int,
    capacity: Int = SizeUnit.create(2, SizeUnit.KiB).toInt(),
    mode: Int = DEFAULT_OPEN_MODE,
): LinuxFileOpenController =
    suspendCancellableCoroutine { callback ->
        try {
            checkScheme(io.hellsinger.filesystem.linux.LinuxOperationOptions.SCHEME)
            val fd = LinuxFileOperationProvider.openFileDescriptor(path.bytes, flags, mode)
            callback.resume(LinuxFileOpenControllerImpl(flags, capacity, fd))
        } catch (exception: Throwable) {
            callback.resumeWithException(exception)
        }
    }

suspend inline fun <P : Path> LinuxFile<P>.buffedOpen(
    flags: Int,
    capacity: Int = SizeUnit.create(2, SizeUnit.KiB).toInt(),
    mode: Int = DEFAULT_OPEN_MODE,
    block: LinuxFileOpenController.() -> Unit,
) = buffedOpen(flags, capacity, mode).apply(block).close()

fun LinuxFileOpenController.bytes(): Flow<Byte> =
    flow {
        var bytes = read()
        while (bytes > 0) {
            flip()
            for (i in 0..bytes) emit(get(i))
            clear()
            bytes = read()
        }
        close()
    }

inline fun LinuxFileOpenController.split(crossinline onDelimitation: (Byte) -> Boolean): Flow<String> =
    flow {
        val builder = StringBuilder()
        bytes().collect { byte ->
            if (!onDelimitation(byte)) {
                builder.append(byte.toInt().toChar())
            } else {
                emit(builder.toString())
                builder.clear()
            }
        }
        // Small file. We've reached end of file, but there's no lines, so we emit
        // data presented in builder
        if (builder.isNotEmpty()) emit(builder.toString())
        builder.clear()
    }

fun LinuxFileOpenController.lines(): Flow<String> =
    split { byte ->
        byte == '\n'.code.toByte()
    }
